using System;
using System.Collections.Generic;
using System.Linq;

using RayDen.Library.Components.Surface;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Core.Types;
using RayDen.RayEngine.Samplers;

namespace RayDen.RayEngine.Engines.PathTracer.Volume
{
    public class UniversalPathSampler : PathTracerPath
    {
        class VolumeInfo
        {
            public RgbSpectrum Sigma_A, Sigma_E;
            public float Distance, S_A;
            public float IndexOfRefraction;

            public float Extinction;
        }

        private readonly Stack<VolumeInfo> volumes;

        public UniversalPathSampler()
        {
            volumes = new Stack<VolumeInfo>(10);
        }

        public override void InitPath(IPathProcessor buffer)
        {
            base.InitPath(buffer);
            volumes.Clear();
            volumes.Push(new VolumeInfo()
            {
                IndexOfRefraction = 1.00005f,
                Sigma_E = new RgbSpectrum(0.5f),
                S_A = 1.64f / 1000f,
                Extinction = new RgbSpectrum(0.5f).y() + 1.64f / 1000f
            });
        }

        public void UpdateVolumeDistance(float d)
        {
            if (volumes.Count == 0)
                return;
            volumes.Peek().Distance = d;
        }

        public float SampleDistance(float u0, float extinct)
        {
            return (float)Math.Log(1f - u0) / extinct;
        }

        protected RgbSpectrum EvalAbsorbance(ref RgbSpectrum thr)
        {
            var res = thr;
            while (volumes.Count > 0)
            {
                var v = volumes.Pop();
                if (v.Distance <= 0f)
                    continue;
                res *= Math.Exp(-v.S_A * v.Distance);

            }
            return res;
        }

        public override void Advance(RayBuffer rayBuffer, SampleBuffer consumer)
        {
            int currentTriangleIndex = 0;
#if VERBOSE

            try {
#endif

            if (((PathState == PathTracerPathState.ShadowRaysOnly) || (PathState == PathTracerPathState.NextVertex)) &&
              (tracedShadowRayCount > 0))
            {
                for (int i = 0; i < tracedShadowRayCount; ++i)
                {
                    RayHit shadowRayHit = rayBuffer.rayHits[secRays[i].ShadowRayIndex];
                    RgbSpectrum attenuation;
                    bool continueTrace;
                    if (this.ShadowRayTest(ref shadowRayHit, ref secRays[i].ShadowRay, out attenuation, out continueTrace))
                    {
                        //if (prevEvent.Has(BsdfEvent.Transmit) && bsdfEvent.Has(BsdfEvent.Transmit))
                        //{
                        //    attenuation*= hitInfo.Color * MathLab.Exp(-Math.Max(shadowRayHit.Distance, Sample.GetLazyValue()*10f));
                        //}
                        Radiance += attenuation * ((secRays[i].Throughput) / secRays[i].Pdf);
                    }

                    if (continueTrace)
                    {
                        continueRays[contCount++] = secRays[i];
                        //continueRays.Add(secRays[i]);
                    }
                }

                if (PathState == PathTracerPathState.ShadowRaysOnly)
                {
                    Splat(consumer);
                    return;
                }

                Array.Copy(continueRays, secRays, contCount);
                tracedShadowRayCount = contCount;
                contCount = 0;
            }



            RayHit rayHit = rayBuffer.rayHits[RayIndex];
            Vector wo = -PathRay.Dir;

            depth++;
            bool missed = rayHit.Index == 0xffffffffu;

            if (missed || depth > scene.MaxPathDepth)
            {
                UpdateVolumeDistance(100f);
                var sampledEnvironment = this.scene.SampleEnvironment(ref wo);
                var thr = EvalAbsorbance(ref Throughput);
                Radiance.MAdd(ref sampledEnvironment, ref thr);

                Splat(consumer);
                return;
            }
            var hitPoint = PathRay.Point(rayHit.Distance);

            if (volumes.Count > 0)
            {
                float d = SampleDistance(Sample.GetLazyValue(), volumes.Peek().Extinction);
                if (d < rayHit.Distance)
                {
                    hitPoint += d * PathRay.Dir;
                    Throughput *= MathLab.Exp(-d * volumes.Peek().Extinction);
                }

            }

            UpdateVolumeDistance(rayHit.Distance);

            var bsdf = EvalIntersection(ref rayHit);
            currentTriangleIndex = (int)rayHit.Index;


            if (hitInfo.IsLight)
            {
                {
                    var lt = scene.GetLightByIndex(currentTriangleIndex);
                    if (lt != null)
                    {
                        float pdf;
                        var le = hitInfo.Color * lt.Emittance(ref wo, out pdf);
                        var thr = EvalAbsorbance(ref Throughput);
                        Radiance.MAdd(ref thr, ref le);
                    }
                }
                Splat(consumer);

                return;
            }


            if (hitInfo.isVolume)
            {
                var vData = new VolumeInfo()
                {
                    IndexOfRefraction = hitInfo.VolumeData.IoR,
                    S_A = hitInfo.VolumeData.Absorbance.y(),
                    Sigma_A = hitInfo.VolumeData.Absorbance,
                    Sigma_E = hitInfo.VolumeData.Scattering,
                    Extinction = hitInfo.VolumeData.Scattering.y() + hitInfo.VolumeData.Absorbance.y()

                };
                var dir = PathRay.Dir;
                var vs = Sample.GetLazyValue();

                if (vData.Sigma_A.Average > vs)
                {
                    this.volumes.Push(vData);
                }
                else
                    if (vData.Sigma_E.Average > vs)
                    {
                        var phaseF = MathLab.INV4PI;
                        var d = volumes.Any() ? Math.Exp(-volumes.Peek().Distance * vData.Extinction) : 1f;

                        RgbSpectrum lightTroughtput = Throughput * hitInfo.Color;
                        LightSampler.EvaluateShadow(ref hitPoint, ref hitInfo.Normal, Sample.GetLDValue(),
                                                    Sample.GetLDValue(), Sample.GetLDValue(), ref ls);
                        float lightStrategyPdf = LightSampler.StrategyPdf;

                        for (int index = 0; index < ls.Length; index++)
                        {

                            if (ls[index].Pdf <= 0f)
                                continue;
                            secRays[tracedShadowRayCount].Throughput = ls[index].Spectrum.GetType() == typeof(RgbSpectrumInfo) ? (RgbSpectrum)(RgbSpectrumInfo)ls[index].Spectrum : (RgbSpectrum)ls[index].Spectrum;
                            secRays[tracedShadowRayCount].Pdf = ls[index].Pdf;
                            secRays[tracedShadowRayCount].ShadowRay = ls[index].LightRay;
                            var fs = phaseF * d * lightTroughtput;
                            secRays[tracedShadowRayCount].Throughput *= fs;
                            if (!secRays[tracedShadowRayCount].Throughput.IsBlack())
                            {
#if DEBUG
                        RayDen.Library.Core.Components.Assert.IsNotNaN(secRays[tracedShadowRayCount].Pdf);
#endif
                                secRays[tracedShadowRayCount].Pdf /= lightStrategyPdf * scene.ShadowRayCount;
                                tracedShadowRayCount++;
                            }
                        }

                        this.volumes.Push(vData);

                        dir = MC.UniformSampleSphere(Sample.GetLazyValue(), Sample.GetLazyValue());
                    }

                PathRay = new RayData(ref hitPoint, ref dir);
                PathState = PathTracerPathState.NextVertex;
                return;
            }

            //Surface
            if (bsdf.IsDiffuse())
            {
                float lightStrategyPdf = LightSampler.StrategyPdf;
                //scene.ShadowRaysPerSample/(float)scene.Lights.Length;
                RgbSpectrum lightTroughtput = Throughput * hitInfo.Color;
                LightSampler.EvaluateShadow(ref hitPoint, ref hitInfo.Normal, Sample.GetLDValue(),
                                            Sample.GetLDValue(), Sample.GetLDValue(), ref ls);
                for (int index = 0; index < ls.Length; index++)
                {

                    if (ls[index].Pdf <= 0f)
                        continue;
                    secRays[tracedShadowRayCount].Throughput = ls[index].Spectrum.GetType() == typeof(RgbSpectrumInfo) ? (RgbSpectrum)(RgbSpectrumInfo)ls[index].Spectrum : (RgbSpectrum)ls[index].Spectrum;
                    secRays[tracedShadowRayCount].Pdf = ls[index].Pdf;
                    secRays[tracedShadowRayCount].ShadowRay = ls[index].LightRay;
                    Vector lwi = secRays[tracedShadowRayCount].ShadowRay.Dir;
                    RgbSpectrum fs;
                    hitInfo.MMaterial.f(ref secRays[tracedShadowRayCount].ShadowRay.Dir, ref wo, ref hitInfo.ShadingNormal, ref Throughput, out fs, types: BrdfType.Diffuse);
                    secRays[tracedShadowRayCount].Throughput *= lightTroughtput *
                                                                Vector.AbsDot(ref hitInfo.Normal, ref lwi) *
                                                                fs;
                    if (!secRays[tracedShadowRayCount].Throughput.IsBlack())
                    {
#if DEBUG
                        RayDen.Library.Core.Components.Assert.IsNotNaN(secRays[tracedShadowRayCount].Pdf);
#endif
                        secRays[tracedShadowRayCount].Pdf /= lightStrategyPdf * scene.ShadowRayCount;
                        tracedShadowRayCount++;
                    }
                }
            }



            float fPdf;
            Vector wi;
            RgbSpectrum f;
            hitInfo.TextureData.Throughput = Throughput;
            prevEvent = bsdfEvent;



            f = bsdf.Sample_f(ref wo, out wi, ref hitInfo.Normal, ref hitInfo.ShadingNormal, ref Throughput,
                                Sample.GetLazyValue(), Sample.GetLazyValue(), Sample.GetLazyValue()
                                , ref hitInfo.TextureData, out fPdf, out bsdfEvent);


            if ((fPdf <= 0.0f) || f.IsBlack())
            {
                if (tracedShadowRayCount > 0)
                    PathState = PathTracerPathState.ShadowRaysOnly;
                else
                {
                    Splat(consumer);

                }
                return;
            }

            if (bsdfEvent.Has(BsdfEvent.Transmit))
            {
                this.volumes.Push(new VolumeInfo()
                {
                    IndexOfRefraction = hitInfo.TextureData.Medium.IoR,
                    S_A = hitInfo.Color.y()
                });
            }
            Throughput *= (f * hitInfo.Color) / fPdf;

            if (depth > scene.MaxPathDepth)
            {
                float prob = Math.Max(Throughput.Filter(), scene.RussianRuletteImportanceCap);
                if (prob >= Sample.GetLazyValue())
                {
                    Throughput /= prob;
                }
                else
                {
                    if (tracedShadowRayCount > 0)
                        PathState = PathTracerPathState.ShadowRaysOnly;
                    else
                    {
                        Splat(consumer);

                    }

                    return;
                }
            }

            PathRay.Org = hitPoint;
            PathRay.Dir = wi.Normalize();
            PathState = PathTracerPathState.NextVertex;


#if VERBOSE
            }
            catch (Exception ex) {
                RayDen.Library.Components.SystemComponents.Tracer.TraceLine("Advance path exception");
                RayDen.Library.Components.SystemComponents.Tracer.TraceLine("Error triangle {0}", currentTriangleIndex);
                RayDen.Library.Components.SystemComponents.Tracer.TraceLine(ex.Message);
                RayDen.Library.Components.SystemComponents.Tracer.TraceLine(ex.StackTrace);

            }
#endif
        }

        protected override bool ShadowRayTest(ref RayHit shadowRayHit, ref RayData shadowRay, out RgbSpectrum attenuation, out bool continueTrace)
        {
            var hit = shadowRayHit.Index == 0xffffffffu || scene.IsLight((int)shadowRayHit.Index);
            attenuation = new RgbSpectrum(1f);
            continueTrace = false;

            if (hit) return true;
            var mesh = scene.GetMeshByTriangleIndex((int)shadowRayHit.Index);
            var mat =
                //SurfaceMaterials.CreateMaterial(scene.MaterialProvider.Get(mesh.MaterialName));
                scene.MatLib.GetSurfMat(mesh != null ? mesh.MaterialName : "default");
            shadowRayEvent = mat.Type.TypeToEvent();

            if (scene.IsVolumeTriangle((int)shadowRayHit.Index))
            {
                continueTrace = true;
                return false;
            }

            if (mat.TransparentShadows)
            {
                continueTrace = true;
                return true;
            }

            //if (shadowRayEvent.Has(BsdfEvent.Transmit))
            //{
            //    var att = MathLab.Exp(-0.1f * shadowRayHit.Distance);
            //    attenuation *= att + att * hitInfo.TextureData.Transmittance;
            //    continueTrace = true;
            //    return true;
            //}

            if (mat.AlphaTexture != null)
            {
                continueTrace = true;
                this.SurfaceSampler.GetIntersection(ref shadowRay, ref shadowRayHit, ref hitInfo, IntersectionOptions.ResolveTextures);
                attenuation = (RgbSpectrum)hitInfo.TextureData.Alpha;
                hit = true;
                //mat.MaterialData.Kt.Filter() < Sample.GetLazyValue();
            }

            return hit;
        }




    }
}